<?php


class Captcha {

    const CODE_LENGTH = 4;

    private $requestId;

    private $imageWidthPx;
    public function getWidth() { return $this->imageWidthPx; }
    public function setWidth($px) { $this->imageWidthPx = $px; }

    private $imageHeightPx;
    public function getHeight() { return $this->imageHeightPx; }
    public function setHeight($px) { $this->imageHeightPx = $px; }

    private $lifetimeMinutes;
    public function getLifetime() { return $this->lifetimeMinutes; }
    public function setLifetime($minutes) { $this->lifetimeMinutes = $minutes; }

    public function getFontsDir() {
        return dirname(__FILE__) . '/fonts/';
    }

    public function __construct($requestId = 'default') {

        $this->requestId = $requestId;

        $this->setWidth(120);
        $this->setHeight(50);
        $this->setLifetime(20);
    }

    /**
     * @return Color
     */
    protected function getDefaultBgColor() {
        return Color::createFromRGBPercents(90, 90, 90);
    }

    public function outputPicture(Color $oBgColor = null) {

        $code = $this->regenerateCode();
        if (!$oBgColor) $oBgColor = $this->getDefaultBgColor();

        $this->output($code, $oBgColor);
    }

    public function checkCodeForErrors($code, ErrorsCollector $collector) {

        $hData = $this->getDataFromSession();

        if (strlen($code) != self::CODE_LENGTH) {
            $this->regenerateCode();
            $collector->addError('Код должен состоять из ' . HumanFormat::numeral(self::CODE_LENGTH, 'символа', 'символов', 'символов'));
            return;
        }

        if (empty($hData[$this->requestId])) {
            $this->regenerateCode();
            $collector->addError('Картинка устарела. Попробуйте ещё раз.');
            return;
        }

        if (strtolower($hData[$this->requestId]['code']) !== strtolower($code)) {
            $this->regenerateCode();
            $collector->addError('Код неверен');
            return;
        }
    }

    public function regenerateCode() {

        $hData = $this->getDataFromSession();
        $code = $this->getNewCode();

        $hData[$this->requestId] = array(
            'time' => time(),
            'code' => $code,
        );

        $this->putDataToSession($hData);

        sleep(1);

        return $code;
    }

    private function getDataFromSession() {

        $hCaptcha = Session::get('hCaptcha');
        if (!$hCaptcha) $hCaptcha = array();

        if (!empty($hCaptcha[$this->requestId]) &&
           ((time() - $hCaptcha[$this->requestId]['time']) > ($this->getLifetime() * Date::MINUTE))
        ) {
            unset($hCaptcha[$this->requestId]);
            $this->putDataToSession($hCaptcha);
        }

        return $hCaptcha;
    }

    private function putDataToSession(array $hData) {
        Session::set('hCaptcha', $hData);
    }

    private function getNewCode() {

        $symbols = '23458yRWxHAmNjFSie';
        $nSymbols = strlen($symbols);

        $code = '';
        for ($i = 0; $i < self::CODE_LENGTH; $i++) {
            $code .= $symbols[mt_rand(0,$nSymbols - 1)];
        }
        return $code;
    }

    private function output($code, Color $bgColor) {

        header('Content-type: image/png');

        $im = imagecreatetruecolor($this->getWidth(), $this->getHeight());

		imagefill($im, 0, 0, $bgColor->getDec());

        $this->addDistortion($im);

        $arrFonts = Dir::getFiles($this->getFontsDir(), '*.ttf');

        if (!$arrFonts)
            throw new Exception('no fonts!');

        $minFontSize = $this->getHeight() * 0.30;
        $maxFontSize = $this->getHeight() * 0.58;

        $minColorBrightnessWord = 100;
        $maxColorBrightnessWord = 150;

        for ($i = 0; $i < self::CODE_LENGTH; $i++) {

            $angle = mt_rand(-10, 10);
            $size = mt_rand($minFontSize,$maxFontSize);
            $margin = 10;

            $x = round($margin * 1.3 + ($i / self::CODE_LENGTH) * ($this->getWidth() - $margin * 2) );
            $y = round($this->getHeight() / 2 + $size / 2);

            $color = $this->getGrayColor($im, mt_rand($minColorBrightnessWord, $maxColorBrightnessWord));
            $font = $arrFonts[mt_rand(0, count($arrFonts) - 1)];

            imagettftext($im, $size, $angle, $x, $y, $color, $font, $code[$i]);
        }

        imagepng($im);
        imagedestroy($im);
    }

    private function addDistortion($im) {

        $minLineLength = 10;
        $maxLineLength = 300;

        $linesNumber = round(0.5 * $this->getWidth() * $this->getHeight() / (($maxLineLength - $minLineLength) / 2));

        $minColorBrightnessLine = 150;
        $maxColorBrightnessLine = 200;

        $maxColorBrightnessLine = 200;

        for ($i = 0; $i < $linesNumber; $i++) {

            imagesetthickness($im, round($i / $linesNumber * 2 + 1));

            $linesLength = mt_rand($maxLineLength, $maxLineLength);

            $y1 = mt_rand(0, $this->getHeight());
            $x1 = mt_rand(0, $this->getWidth());

            $x2 = mt_rand($x1 - $linesLength, $x1 + $linesLength);
            $y2 = mt_rand($y1 - $linesLength, $y1 + $linesLength);

            $color = $this->getGrayColor($im, mt_rand($minColorBrightnessLine, $maxColorBrightnessLine));

            imageline($im, $x1, $y1, $x2, $y2, $color);
        }
    }

    private function getGrayColor($im, $color) {

        static $arrGray = array();

        if (!isset($arrGray[$color])) {
            $arrGray[$color] = imagecolorallocate($im,
                $color + mt_rand(-1,1),
                $color + mt_rand(-1,1),
                $color + mt_rand(-1,1)
            );
        }

        return $arrGray[$color];
    }
}

